package logic

import (
	"bytes"
	"errors"
	"regexp"
	"struggle/dao/mysql"
	"struggle/model"
	"unicode/utf8"

	"github.com/yuin/goldmark/text"
	"go.uber.org/zap"

	"github.com/alecthomas/chroma/formatters/html"
	stats "github.com/mdigger/goldmark-stats"
	toc "github.com/mdigger/goldmark-toc"
	"github.com/yuin/goldmark"
	highlighting "github.com/yuin/goldmark-highlighting"
	"github.com/yuin/goldmark/extension"
)

type articleLogic struct{}

var DefaultArticle = articleLogic{}

var extractCoverReg = regexp.MustCompile(`!\[.*\]\((.*)\)`)

// Publish 发布文章
// 防止 XSS 攻击 --> `html/template` 内容检测和转义
// 敏感词过滤 --> 涉黄、涉恐等屏蔽词
func (a articleLogic) Publish(article *model.Article) (err error) {
	// 1. 通过正则（extractCoverReg）提取正文中第一张图作为文章封面（如果有的话）
	matched := extractCoverReg.FindStringSubmatch(article.Content)
	if len(matched) > 1 {
		article.Cover = matched[1]
	}
	// 2. 计算字数，不能使用 len，因为它计算的是字节数，utf8.RuneCountInString 能满足我们的需求。
	// TODO: 当然这里没有对干扰字符做处理，比如：图片、链接等；
	article.Words = utf8.RuneCountInString(article.Content)
	// 3. 准备好了文章需要的数据，接着就是通过 DAO 层入库
	err = mysql.DefaultArticle.Create(article)
	if err != nil {
		zap.L().Error("mysql.CreateArticle failed", zap.Error(err))
		return
	}
	return
}

// Update 更新个人文章
func (a articleLogic) Update(article *model.Article) (err error) {
	articleResp, err := mysql.DefaultArticle.FindArticle(article.Sn)
	if err != nil {
		zap.L().Error("Update Article logic failed", zap.Error(err))
		return
	}
	if articleResp.UID != article.UID {
		return errors.New("你不是本篇文章作者")
	}
	return mysql.DefaultArticle.Update(article)
}

// FindUserArticle 获取个人文章列表
func (a articleLogic) FindUserArticle(sn uint64, offset, limit int) (articleInfoList *[]model.ArticleInfo, err error) {
	articleInfoList, err = mysql.DefaultArticle.FindUserArticle(sn, offset, limit)
	if err != nil {
		zap.L().Error("mysql.DefaultArticle.Find failed", zap.Error(err))
		return
	}
	if len(*articleInfoList) == 0 {
		zap.L().Info("article list is null")
		return
	}
	return
}

// Find 获取文章列表
func (a articleLogic) Find(offset, limit int) (apiArticleList *model.ApiArticleList, err error) {
	articleList, err := mysql.DefaultArticle.Find(offset, limit)
	if err != nil {
		zap.L().Error("mysql.DefaultArticle.Find failed", zap.Error(err))
		return
	}
	if len(articleList) == 0 {
		zap.L().Info("article list is null")
		return
	}

	var userIDList []uint64
	for _, v := range articleList {
		userIDList = append(userIDList, v.UID)
	}
	userInfoList, err := mysql.GetUserInfoList(userIDList)
	if err != nil {
		zap.L().Error("mysql.GetUserInfoList failed", zap.Any("userIDList", userIDList), zap.Error(err))
		return
	}
	zap.L().Debug("GetArticleList", zap.Any("articleList", articleList), zap.Any("userInfoList", userInfoList))
	// 涉及到大量数据的 slice, 如果知道数据的数量，应当使用 make 预先初始化, 比 append() 扩容效率更高
	apiArticleList = &model.ApiArticleList{
		ArticleList: make([]*model.ApiArticle, 0, len(articleList)),
	}

	for _, v := range articleList {
		apiArticle := &model.ApiArticle{
			ArticleInfo: *v,
		}
		for _, user := range userInfoList {
			if user.UserSn == v.UID {
				apiArticle.AuthorName = user.Email
				break
			}
		}
		apiArticleList.ArticleList = append(apiArticleList.ArticleList, apiArticle)
	}
	count, err := mysql.DefaultArticle.GetArticleCount()
	if err != nil {
		zap.L().Error("mysql.GetQuestionCount failed", zap.Error(err))
		return
	}
	apiArticleList.TotalCount = count
	return
}

// FindOne 获取文章详情包括 Article 和 ArticleEx 信息
func (a articleLogic) FindOne(sn uint64, clientParse bool) (*model.ArticleInfo, *model.Catalog, *model.ArticleState, error) {
	article, err := mysql.DefaultArticle.FindOne(sn)
	if err != nil {
		zap.L().Error("mysql.DefaultArticle FindOne failed", zap.Error(err))
		return nil, nil, nil, err
	}
	// 文本统计
	doc := goldmark.DefaultParser().Parse(text.NewReader([]byte(article.Content)))
	info := stats.New(doc, []byte(article.Content))
	articleState := &model.ArticleState{
		Chars:   info.Chars,                  // 中文主要看 chars 字段，表示多少个字；
		Reading: info.Duration(400).String(), // 阅读时间，我们按照一分钟 400 个字算，需要 3m9s；
	}

	if clientParse { // 由客户端解析 Markdown
		return article, nil, articleState, nil
	}
	// 自定义解析器
	markdown := goldmark.New(
		goldmark.WithExtensions(extension.GFM), // 支持 GFM
		goldmark.WithExtensions( // 语法高亮插件
			highlighting.NewHighlighting(
				highlighting.WithStyle("monokai"),
				highlighting.WithFormatOptions(
					html.WithLineNumbers(true)))),
	)
	var buf bytes.Buffer
	// 添加 TOC 目录
	convertFunc := toc.Markdown(markdown)
	headers, err := convertFunc([]byte(article.Content), &buf)
	if err != nil {
		zap.L().Error("服务端解析 Markdown 生成目录错误", zap.Error(err))
		return nil, nil, nil, err
	}
	article.HTML = buf.String()
	// 构建目录结构，最多两级目录
	catalog := &model.Catalog{
		Items: make([]*model.Item, 0, 8),
	}
	for i, length := 0, len(headers); i < length; i++ {
		header := headers[i]
		item := &model.Item{
			Level: header.Level,
			ID:    header.ID,
			Text:  header.Text,
			Child: make([]*model.Item, 0, 4),
		}
		// 下一个标题级别更高，表明是子标题
		if i < length-1 && headers[i+1].Level > item.Level {
			j := i + 1
			for ; j < length; j++ {
				childItem := &model.Item{
					Level: headers[j].Level,
					ID:    headers[j].ID,
					Text:  headers[j].Text,
				}
				item.Child = append(item.Child, childItem)
				if j < length-1 {
					if headers[j+1].Level > childItem.Level {
						// 下一个级别更高，子标题，跳过
						j++
						break
					} else if headers[j+1].Level < childItem.Level {
						// 下一个级别更低，和父标题同级
						break
					}
				}
			}
			i += j - i
		}
		catalog.Items = append(catalog.Items, item)
	}
	return article, catalog, articleState, nil
}

// 给指定文章或问题的阅读数+1
func (a articleLogic) IncReadCount(sn uint64, ip string, objType int) (err error) {
	view := &model.ViewMeta{
		Ip:         ip,
		ObjectType: objType, // 文章是类型0, 问题类型是1
		ObjectId:   sn,
	}
	err = mysql.DefaultView.Create(view)
	if err != nil {
		zap.L().Error("给指定文章或问题的阅读数+1 failed", zap.Any("view", view), zap.Error(err))
		return
	}
	return
}
